package main

import (
    "fmt"
    "encoding/json"
    "encoding/base64"
    
    "github.com/hyperledger/fabric-chaincode-go/pkg/statebased"
	"github.com/hyperledger/fabric-chaincode-go/shim"
	"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// Two aspects can be optimized:
// 1. attibuted base authention via certificate
// 2. use private data for confidentail payment

const (
    BANKORG = "BankOrgMSP"
    ENTERPRISEORG = "EnterpriseOrgMSP"
)

const (
    CREATED = "created"
    REQUESTED = "requested"
    APPROVED = "approved"
    CONFIRMED  = "confirmed"
)

type User struct {
    UserId   string `json:"userId"`
	Name     string `json:"name"`	
	Org      string `json:"org"` 
}

type Payment struct {
    Code              string `json:"code"`
    UserId            string `json:"userId"`
    Status            string `json:"status"`
}

type SmartContract struct {
    contractapi.Contract
}

func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
	return nil
}

// query user method
func (s *SmartContract) QueryUser(ctx contractapi.TransactionContextInterface, userID string) (*User, error) {
	userJSON, err := ctx.GetStub().GetState(userID)
	if err != nil {
		return nil, fmt.Errorf("failed to read from world state: %v", err)
	}
	if userJSON == nil {
		return nil, fmt.Errorf("%s does not exist", userID)
	}

	var user *User
	err = json.Unmarshal(userJSON, &user)
	if err != nil {
		return nil, err
	}
	return user, nil
}

// query payment method
func (s *SmartContract) QueryPayment(ctx contractapi.TransactionContextInterface, paymentCode string) (*Payment, error) {
	paymentJSON, err := ctx.GetStub().GetState(paymentCode)
	if err != nil {
		return nil, fmt.Errorf("failed to read from world state: %v", err)
	}
	if paymentJSON == nil {
		return nil, fmt.Errorf("%s does not exist", paymentCode)
	}

	var payment *Payment
	err = json.Unmarshal(paymentJSON, &payment)
	if err != nil {
		return nil, err
	}
	return payment, nil
}

//request payment method
func (s *SmartContract) RequestPayment(ctx contractapi.TransactionContextInterface, paymentCode string) error {
    clientOrgID, err := getClientOrgID(ctx, true)
	if err != nil {
		return fmt.Errorf("failed to get verified OrgID: %v", err)
	}
    payment, err := s.QueryPayment(ctx, paymentCode)
    if err != nil {
		return fmt.Errorf("failed to get payment: %v", err)
	}
    user, err := s.QueryUser(ctx, payment.UserId)
    if err != nil {
		return fmt.Errorf("failed to request payment: %v", err)
	}
    if pament.Status != CREATED {
        return fmt.Errorf("failed to request payment: %v", CREATED)
    }
    err = setStateBasedEndorsement(ctx, paymentCode, BANKORG)
	if err != nil {
		return fmt.Errorf("failed setting state based endorsement for payment: %v", err)
	}
    pament.Status = REQUESTED
    updatedPaymentJSON, err := json.Marshal(payment)
    if err != nil {
        return fmt.Errorf("failed to marshal payment: %v", err)
    }
    return ctx.GetStub().PutState(paymentCode, updatedPaymentJSON)
}

// approve payment method
func (s *SmartContract) ApprovePayment(ctx contractapi.TransactionContextInterface, paymentCode string) error {
    clientOrgID, err := getClientOrgID(ctx, true)
	if err != nil {
		return fmt.Errorf("failed to get verified OrgID: %v", err)
	}
    payment, err := s.QueryPayment(ctx, paymentCode)
    if err != nil {
		return fmt.Errorf("failed to get payment: %v", err)
	}
    if pament.Status != REQUESTED {
        return fmt.Errorf("failed to request payment: %v", REQUESTED)
    }
    err = setStateBasedEndorsement(ctx, paymentCode, ENTERPRISEORG)
	if err != nil {
		return fmt.Errorf("failed setting state based endorsement for owner: %v", err)
	}
    pament.Status = APPROVED
    updatedPaymentJSON, err := json.Marshal(payment)
    if err != nil {
        return fmt.Errorf("failed to marshal payment: %v", err)
    }
    return ctx.GetStub().PutState(paymentCode, updatedPaymentJSON)
}

// confirm payment method
func (s *SmartContract) ConfirmPayment(ctx contractapi.TransactionContextInterface, paymentCode string) error {
    clientOrgID, err := getClientOrgID(ctx, true)
	if err != nil {
		return fmt.Errorf("failed to get verified OrgID: %v", err)
	}
    payment, err := s.QueryPayment(ctx, paymentCode)
    if err != nil {
		return fmt.Errorf("failed to get payment: %v", err)
	}
    if pament.Status != APPROVED {
        return fmt.Errorf("failed to request payment: %v", APPROVED)
    }
    pament.Status = CONFIRMED
    updatedPaymentJSON, err := json.Marshal(payment)
    if err != nil {
        return fmt.Errorf("failed to marshal payment: %v", err)
    }
    return ctx.GetStub().PutState(paymentCode, updatedPaymentJSON)
}

// add state based endorsement policy
func setStateBasedEndorsement(ctx contractapi.TransactionContextInterface, paymentID string, orgToEndorse string) error {
	endorsementPolicy, err := statebased.NewStateEP(nil)
	if err != nil {
		return err
	}
	err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgToEndorse)
	if err != nil {
		return fmt.Errorf("failed to add org to endorsement policy: %v", err)
	}
	policy, err := endorsementPolicy.Policy()
	if err != nil {
		return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err)
	}
	err = ctx.GetStub().SetStateValidationParameter(paymentID, policy)
	if err != nil {
		return fmt.Errorf("failed to set validation parameter on payement: %v", err)
	}
	return nil
}

// client and peer authention
func getClientOrgID(ctx contractapi.TransactionContextInterface, verifyOrg bool) (string, error) {
	clientOrgID, err := ctx.GetClientIdentity().GetMSPID()
	if err != nil {
		return "", fmt.Errorf("failed getting client's orgID: %v", err)
	}
	if verifyOrg {
		err = verifyClientOrgMatchesPeerOrg(clientOrgID)
		if err != nil {
			return "", err
		}
	}
	return clientOrgID, nil
}

func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
	peerOrgID, err := shim.GetMSPID()
	if err != nil {
		return fmt.Errorf("failed getting peer's orgID: %v", err)
	}
	if clientOrgID != peerOrgID {
		return fmt.Errorf("client from org %s is not authorized to read or write private data from an org %s peer",
			clientOrgID,
			peerOrgID,
		)
	}
	return nil
}

func (s *SmartContract) GetSubmittingClientIdentity(ctx contractapi.TransactionContextInterface) (string, error) {
	b64ID, err := ctx.GetClientIdentity().GetID()
	if err != nil {
		return "", fmt.Errorf("Failed to read clientID: %v", err)
	}
	decodeID, err := base64.StdEncoding.DecodeString(b64ID)
	if err != nil {
		return "", fmt.Errorf("failed to base64 decode clientID: %v", err)
	}
	return string(decodeID), nil
}

func main() {
    chaincode, err := contractapi.NewChaincode(new(SmartContract))
	if err != nil {
		fmt.Printf("Error create payment chaincode: %v", err)
	}
	if err := chaincode.Start(); err != nil {
		fmt.Printf("Error starting payment chaincode: %v", err)
	}    
}